'use strict'

const marked = require('marked')
const fs = require('fs')
const os = require('os')
const zlib = require('zlib')

/**
 * 默认递归目录最高10层，只要是.md文件就会加载，
 * 默认 _ 和 . 开头的目录不加载。
 * 在markdown文件中，文件首部要使用注释来说明元信息：
 *  <!--name:[NAME]-->
 *  <!--description:[DESCRIPTION]-->
 *  <!--author:[AUTHOR]-->
 *  <!--time:[TIME]-->
 *  <!--keywords:[KEYWORDS]-->
 *  <!--headimg:[IMAGE-PATH]-->
 *  headimg涉及路径，皆以当前.md文件所在路径作为相对路径。
 *  约定如果文件名称有@，则后面是时间戳，格式为2019-10-24
 */

class mdoc {

  constructor (options = {}) {
    //\n\n解析成<br>
    this.parsetn = false

    this.docpath = ''

    this.groups = {}

    //关键词到文件路径的映射
    this.keyFile = new Map()

    //实际文件数据，文件路径到文件数据的映射
    this.fileData = new Map()

    //使用哪一级目录作为分组
    this.grpLevel = 0

    this.fileinfo = new Map()

    this.author = ''

    this.pstyle = ''

    /**
     * 会添加在image路径后的参数pre=
     * 主要为了在指定加载路径，此时localimage接口要根据路径去讯寻找图片导致路径错误，
     * 此时可以根据相对于docpath的路径，把这个路径作为参数传递。
     */
    this.imgpre = ''

    this.domain = ''

    //最大50K
    this.maxSize = 51200

    this.maxCount = 1000000

    this.kkeys = []

    this.total = 0

    //要排除的目录
    this.filter = []

    this.imgqs = ''

    if (options.toString() !== '[object Object]') {
      options = {}
    }

    for (let k in options) {
      switch (k) {
        case 'imgpre':
        case 'imgqs':
        case 'maxSize':
        case 'domain':
        case 'grpLevel':
        case 'docpath':
        case 'parsetn':
        case 'filter':
        case 'author':
        case 'pstyle':
          this[k] = options[k]
          break
      }
    }

    //不能超过总内存的50%
    this.totalMaxSize = parseInt(os.totalmem() * 0.5)

    this.totalSize = 0

    this.groupList = []

    this.keyLecture = {}
    this.lectureList = []
    this.lectureKey = {}
  }

  async init(dirn = '') {
    await this.loaddir(dirn || this.docpath, '', 0)

    let keys = this.keyFile.keys()

    for (let k of keys) {
      this.kkeys.push(k)
    }

    this.groupList = Object.keys(this.groups)
    
  }

  clear () {
    this.groups = {}

    //关键词到文件路径的映射
    this.keyFile.clear()

    //实际文件数据，文件路径到文件数据的映射
    this.fileData.clear()

    this.fileinfo.clear()

    this.kkeys = []

    this.total = 0

    this.totalSize = 0

    this.groupList = []

    this.keyLecture = {}
    this.lectureList = []
    this.lectureKey = {}

  }

  async loaddir (pdir, pkg = '', deep = 0) {

    if (deep > 10) {
      return
    }

    let configData = {
      keywords : ''
    }

    try {
      let dlist = fs.readdirSync(pdir, {withFileTypes: true})

      let kid = ''

      for (let i = 0; i < dlist.length; i++) {

        if (deep === 0 && this.filter.indexOf(dlist[i].name) >= 0) {
          continue
        }

        if (dlist[i].name[0] == '.' 
          || dlist[i].name[0] == '!'
          || (deep = 0 && dlist[i].name == 'RAEDME.md'))
        {
          continue
        }

        try {
          fs.accessSync(`${pdir}/config.json`)
          configData = require(`${pdir}/config.json`)
          if (!configData.keywords) {
            configData.keywords = ''
          }
        } catch (err) {

        }

        if (dlist[i].isDirectory()) {

          kid = `${pkg}${pkg===''?'':'/'}${dlist[i].name}`

          await this.loaddir(`${pdir}/${dlist[i].name}`, kid, deep+1)

        } else if (dlist[i].isFile()) {

          await this.loaddoc(pdir, pkg, dlist[i].name, configData)

        }
      }
    } catch (err) {
      console.error(err.message)
    }
  }

  async loaddoc (pdir, pkg, pname, cfg) {
    if (pname.length < 4 || pname.substring(pname.length - 3) !== '.md') {
      return
    }

    try {
      let fst = fs.statSync(`${pdir}/${pname}`)

      if (fst.size > this.maxSize) {
        console.error(`忽略读取超过${parseInt(this.maxSize/1000)}Kb的文件`)
        return
      }

      let fname = `${pdir}/${pname}`
      
      let data = fs.readFileSync(fname, {encoding: 'utf8'})

      await this.parsedoc(data, pkg, pname.substring(0, pname.length-3), cfg)

    } catch (err) {
      console.error(err)
    }
  }

  /**
   * 
   * @param {*} data markdown文本数据
   * @param {*} pkgdir 所在目录
   * @param {*} pname 名字
   */
  async parsedoc (data, pkgdir, pname, cfg) {

    if (this.total >= this.maxCount) {
      console.error(`最大记录${this.maxCount}条数据。`)
      return false
    }

    if (this.totalSize >= this.totalMaxSize) {
      console.error(`最大使用${(this.totalMaxSize / 1000000).toFixed(2)}M内存。`)
      return false
    }

    let di = {
      name : pname,
      description: '',
      keywords: '',
      author: '',
      time : '',
      headimg : ''
    }

    let tmp = ''
    let r = null
    let lastind = 0

    for (let k in di ) {
      r = new RegExp(`^<\!--${k}:.*-->$`, 'im')
      tmp = data.match(r)

      if (tmp === null) {
        continue
      }

      lastind = tmp.index + tmp[0].length

      di[k] = tmp[0].substring(5+k.length, tmp[0].length-3).trim()
    }

    if (lastind > 0) {
      data = data.substring(lastind).trim()
    }

    di.id = `${pkgdir}${pkgdir=='' ? '' : '/'}${pname}`

    if (data.length == 0) {
      return
    }

    if (di.headimg.length > 0) {
      di.headimg = `${pkgdir}${pkgdir=='' ? '' : '/'}${di.headimg}`
    }

    if (di.time === 'now') {
      di.time = formatTime(null, true)

    } else if (pkgdir.indexOf('@') >= 0) {
      di.time = pkgdir.split('@')[1]
    }

    let kstr = `${di.id}:${di.name == pname ? '' : di.name}:`
      + (cfg.keywords ? `${cfg.keywords}:` : '')
      + `${di.keywords.substring(0, 64)}`

    this.fileinfo.set(di.id, di)

    //直接索引
    this.keyFile.set(kstr, di.id)

    //第一层目录作为分组
    if (di.id.indexOf('/') > 0) {
      let grparr = di.id.split('/')
      let grp = grparr[0]

      if (grparr.length-1 > this.grpLevel) {
        grp = grparr[this.grpLevel]
      }

      if (this.groups[grp] === undefined) {
        this.groups[grp] = []
      }

      this.groups[grp].push(kstr)
    }
    
    let mdata = {
      name : di.name,
      description: di.description,
      keywords: di.keywords,
      author: di.author,
      time : di.time,
      headimg: di.headimg,
      data : this.markdata(data, pkgdir)
    }

    let zipdata = await new Promise((rv, rj) => {

      zlib.gzip(JSON.stringify(mdata), (err, zipdata) => {
          if (err) {
            rj (err)
          }

          rv(zipdata)
      })

    })

    this.fileData.set(di.id, zipdata)
    this.total += 1
    this.totalSize += zipdata.length
  }

  markdata (mdata, pkgdir) {

    let opts = {
      breaks:true,
      gfm: true
    }
    
    mdata = this.replaceImageSrc(mdata, pkgdir)

    let htmldata = marked(mdata, opts)

    htmldata = this.setImageStyle(htmldata)

    if (this.pstyle) {
      htmldata = htmldata.replace(/\<p\>/ig, `<p style="${this.pstyle}">`)
    }

    htmldata = htmldata.replace(/<\/pre>/ig, '</pre><br>')

    if (this.parsetn) {
      htmldata = htmldata.replace(/<\/p>/ig, '</p><br>')
    }
    
    return htmldata
  }

  parseCond(cstr) {
    return new RegExp(cstr, 'i')
  }

  resetImgSrc (data, imgsrc, pkgdir) {
    let realsrc = ''

    if (imgsrc[0] == '/') {
      imgsrc = imgsrc.substring(1)
    }

    realsrc = `${this.domain}/md/image/${pkgdir}/${imgsrc}`

    if (this.imgpre.length > 0) {
      realsrc += '?pre=' + this.imgpre
    }

    if (this.imgqs.length > 0) {
      realsrc += realsrc.indexOf('?') > 0 ? '&' : '?'
      realsrc += this.imgqs
    }

    let i = 0

    while ( data.indexOf(`](${imgsrc}`) >= 0  && i < 120) {
      data = data.replace(imgsrc, realsrc)
      i+=1
    }

    return data
  }

  replaceImageSrc (data, pkgdir) {
    let images = data.match(/\!\[[^\]]*\]\(.*\)/ig)
    
    if (!images) {
      return data
    }

    let tmp = ''

    for(let i=0; i<images.length; i++) {
      tmp = images[i].split('](')[1]

      data = this.resetImgSrc(data, tmp.substring(0, tmp.length-1), pkgdir)
    }

    return data
  }

  setImageStyle (html) {
    return html.replace(/\<img /ig, '<img loading=lazy style="width:auto;height:auto;max-width:100%;" ')
  }

  group () {
    return this.groupList
  }

  randget () {
    let k = this.kkeys[ parseInt(this.kkeys.length * Math.random()) ]
    let kid = this.keyFile.get(k)
    let kdata

    if (kid) {
      kdata = this.fileData.get(kid)
    }

    return kdata || null
  }

  getById (id, justinfo = false) {
    if (justinfo) {
      if (this.fileinfo.has(id)) {
        return this.fileinfo.get(id)
      }
      return null
    }

    if (this.fileData.has(id)) {

      return this.fileData.get(id)
    }

    return null
  }

  get (cstr, offset = 0, justinfo = false) {
    let r = this.search(cstr, 1, offset)

    if (r.length > 0) {
      return this.fileData.get(r[0].id)
    }

    return null
  }

  select (cstr, limit = 0, offset = 0, group=null, withData = false) {
    if (withData) {
      let ret = this.search(cstr, limit, offset, group)

      for (let i=0; i < ret.length; i++) {
        ret[i] = this.fileData.get(ret[i].id).toString()
      }

      return ret
    }

    return this.search(cstr, limit, offset, group)
  }

  count (cstr, group = null) {
    if ((cstr === '' || cstr === null) && group === null) {
      return this.total
    }

    let preg = this.parseCond(cstr)
    let total = 0
    let grpkeys = this.kkeys

    if (group !== null) {
      if (this.groups[group] === undefined) {
        return 0
      }

      grpkeys = this.groups[group]
    }

    if (cstr === '' || cstr === null) {
      return grpkeys.length
    }

    for (let i=0; i < grpkeys.length; i++) {
      if ( preg.test(grpkeys[i]) ) {
        total += 1
      }
    }
    return total
  }

  search (cstr, limit = 0, offset = 0, group = null) {
    let preg = this.parseCond(cstr)
    let result = []
    let count = 0
    let start = 0
    let tmp = ''

    let grpkeys = this.kkeys

    if (group !== null) {
      if (this.groups[group] === undefined) {
        return []
      }
      grpkeys = this.groups[group]
    }

    for (let i=0; i < grpkeys.length; i++) {
      if ( !preg.test(grpkeys[i]) ) {
        continue
      }
      
      tmp = this.fileinfo.get(this.keyFile.get(grpkeys[i]) )
      start += 1

      if (start > offset) {
        if (limit <= 0) {
          count += 1
          result.push({
              time : tmp.time,
              id: tmp.id,
              author: tmp.author,
              name: tmp.name,
              description: tmp.description,
              headimg : tmp.headimg
          })

        } else if (limit > 0) {
          if (count < limit) {
            count += 1
            result.push({
                time : tmp.time,
                id: tmp.id,
                author: tmp.author,
                name: tmp.name,
                description: tmp.description,
                headimg : tmp.headimg
            })
          } else {
            return result
          }
        }

      }
    }

    return result
  }

  initLecture (lpath = '') {
    let lecpath = (lpath || this.docpath) + '/_lecture'
    let lecjson = lecpath + '/@lecture.json'
    let lecList = ''

    try {
      fs.accessSync(lecpath, fs.constants.F_OK)
      fs.accessSync(lecjson, fs.constants.F_OK)
      lecList = JSON.parse(fs.readFileSync(lecjson))
    } catch (err) {
      return false
    }

    if (!lecList.list || typeof lecList.list !== 'object') {
      return false
    }
    
    for (let i=0; i < lecList.list.length; i++) {
      this.loadLecture(
          `${lecpath}/${lecList.list[i]}.json`,
          lecList.list[i],
          lpath || this.docpath)
    }

  }

  lectures () {
    return this.lectureList
  }

  getLec (cstr, offset = 0) {
    let r = this.searchLec(cstr, 1, offset)
    if (r.length == 0) {
      return null
    }

    return r[0]
  }

  getLecById(lecid) {
    if (this.lectureKey[lecid]) {
      return this.lectureKey[lecid]
    }
    return null
  }

  selectLec (cstr, limit = 10, offset = 0) {
    return this.searchLec(cstr, limit, offset)
  }

  searchLec (cstr, limit = 0, offset = 0) {
    let preg = this.parseCond(cstr)
    let result = []
    let tmp = ''
    let start = 0
    let count = 0

    for (let k in this.keyLecture) {
      if (!preg.test(k)) {
        continue
      }

      tmp = this.lectureKey[ this.keyLecture[k] ]
      start += 1

      if (start <= offset) {
        continue
      }

      if (limit <= 0) {
        result.push({
            id : tmp.id,
            name : tmp.name,
            description: tmp.description,
            author: tmp.author,
            list : tmp.list,
            image : tmp.image
        })
      } else {
        if (count < limit) {
            count += 1
            result.push({
                id: tmp.id,
                author: tmp.author,
                name: tmp.name,
                description: tmp.description,
                list : tmp.list,
                image : tmp.image
            })
        } else {
          return result
        }
      }
    }

    return result
  }

  loadLecture (lecfile, leckey, lpath = '') {
    if (lpath === '') {
      lpath = this.docpath
    }

    let lecdata = ''

    try {
      fs.accessSync(lecfile, fs.constants.F_OK)
      lecdata = JSON.parse(fs.readFileSync(lecfile))
    } catch (err) {
      console.error(err)
      return false
    }

    if (!lecdata.name || lecdata.name.trim() == '') {
      return false
    }

    if (!lecdata.image) {
      lecdata.image = ''
    } else {
      lecdata.image = this.domain + '/md/image/' + lecdata.image
    }
    
    let lecobj = {
      id : leckey,
      name: lecdata.name.trim(),
      list: [],
      author: lecdata.author || this.author,
      description: lecdata.description,
      image : lecdata.image
    }

    let files = null
    let lecpkgnames = []

    
    if (lecdata.listdir) {
      try {
        files = fs.readdirSync(`${lpath}/${lecdata.listdir}`, {withFileTypes:true})

        for(let i=0; i<files.length; i++) {
          if (!files[i].isFile()) {
            continue
          }
          if (files[i].name.substring(files[i].name.length-3) !== '.md') {
            continue
          }

          lecpkgnames.push(`${lecdata.listdir}/${files[i].name.substring(0, files[i].name.length-3)}`)
        }
      } catch (err) {
        console.error(err)
        return false
      }
    } else if (lecdata.list) {
      lecpkgnames = lecdata.list
    } else {
      return false
    }

    lecpkgnames.sort((a, b) => {
      if (a > b) {
        return 1
      }

      if (a < b) {
        return -1
      }

      return 0
    })

    for (let c of lecpkgnames) {
      if (c[c.length-1] == '/') {
        c = c.substring(0 , c.length - 1)
      }
      if (this.fileinfo.has(c)) {
        lecobj.list.push({
          name : this.fileinfo.get(c).name,
          id : c
        })
      }
    }

    this.lectureKey[leckey] = lecobj

    this.lectureList.push({
      name : lecdata.name,
      id : leckey,
      author: lecdata.author,
      description: lecdata.description,
      image : lecdata.image
    })

    this.keyLecture[leckey+lecdata.name] = leckey
  }

}

module.exports = mdoc
